/**
 * @copyright Copyright (c) 2024 COOREAL. All rights reserved.
 * @author wcfan @date 2024-08-07
 * @description 提供一个 COOREAL 主键忽略大小写的内部数据字典类。
 */

import _ from 'lodash';
// 定义一个内部对象类型
export type KrRecordable<T = any> = Record<string, T>;
/**
 * 将对象类型 T 的所有键转换为小写字母的类型。
 * @template T - 要转换键的对象类型。
 * @returns 一个新类型，其键为 T 的键的小写形式，值与 T 的值相同。
 */
export type KrLowercaseKeys<T> = {
  [K in keyof T as Lowercase<string & K>]: T[K];
};

// 使用Symbol来创建一个唯一的私有键，防止被Object.keys(this)列出
const normalizeKeySymbol = Symbol('normalizeKey');

export class KrData<T extends KrRecordable = KrRecordable> {
  private [normalizeKeySymbol]: boolean;

  /**
   * 创建一个新的实例。
   * @param { any } records - 可选的记录数据，类型为 T。
   * @param { { normalizeKey?: boolean } } options - 可选的配置选项。
   * @param { boolean } options.normalizeKey 指示是否规范化小写键，默认为 true。
   */
  constructor(records?: T, options?: { normalizeKey?: boolean }) {
    this[normalizeKeySymbol] = options?.normalizeKey ?? true;
    if (records) {
      this.addRange(records);
    }
  }

  /**
   * 批量添加记录。
   * @param { T } records - 要添加的记录对象，其中键为记录的标识符，值为记录的内容。
   */
  public addRange(records: T) {
    Object.entries(records).forEach(([key, value]) => this.set(key, value));
  }

  /**
   * 获取指定键的值。
   * @param { string } key - 要获取值的键。
   * @returns { any} 返回键对应的值，如果键不存在，则返回 `undefined`。
   */
  public get<T = KrRecordable>(key: string): T {
    const normalizedKey = this.getNormalizeKey(key);
    return (this as KrRecordable)[normalizedKey] as T;
  }

  /**
   * 根据提供的路径获取对象中的值。
   * @param { string } path - 要查找的路径，可以是对象的属性路径。
   * @returns { any } 如果路径存在，返回对应的值；否则返回 `undefined`。
   */
  public getPath(path: string): T[keyof T] | undefined {
    const normalizedKey = this.getNormalizeKey(path);
    return _.get(this, normalizedKey); // 从私有的 data 获取值
  }

  /**
   * 设置指定键的值。
   * @param { string } key - 要设置的键，类型为字符串。
   * @param { any } value - 与键关联的值，可以是任何类型。
   * 此方法会根据键生成一个标准化的键值，并将给定的值设置到该标准化键上。
   */
  public set(key: string, value: any): void {
    const normalizedKey = this.getNormalizeKey(key);
    (this as KrRecordable)[normalizedKey] = value;
  }

  /**
   * 设置指定路径的值。
   * 此方法通过规范化给定的路径，并使用 `_.set` 将值设置到对象中。
   * @param { string } path - 要设置的路径，通常是对象的键路径，支持嵌套路径。
   * @param { any } value - 要设置的值，可以是任何类型。
   */
  public setPath(path: string, value: any): void {
    const normalizedKey = this.getNormalizeKey(path);
    _.set(this, normalizedKey, value);
  }

  /**
   * 删除指定的键。
   * @param {string} key - 需要删除的键。
   */
  public remove(key: string): void {
    const normalizedKey = this.getNormalizeKey(key);
    Reflect.deleteProperty(this, normalizedKey); // 删除私有的 data 变量中的键
  }

  /**
   * 根据路径删除对象中的键（支持嵌套路径）。
   * @param {string} path - 需要删除的键的路径，例如 `"user.address.city"`。
   */
  public removePath(path: string): void {
    const normalizedKey = this.getNormalizeKey(path);
    _.unset(this, normalizedKey);
  }

  /**
   * 清空当前对象的所有键值。
   */
  public clear(): void {
    Object.keys(this).forEach((key) => {
      this.remove(key);
    });
  }

  /**
   * 获取当前对象的所有值，并确保所有键都是小写格式。
   * @returns {KrLowercaseKeys<T>} - 规范化键名（小写）的数据对象。
   */
  public getLowercaseValues(): KrLowercaseKeys<T> {
    const data: KrRecordable = {};
    for (const key of Object.keys(this)) {
      data[key] = this.get(key);
    }
    return data as KrLowercaseKeys<T>;
  }

  /**
   * 获取当前对象的所有键名，保持原始键名格式。
   * @returns {Array<string>} - 原始键名的数组。
   */
  public getKeys() {
    return Object.keys(this);
  }

  public has(key: string): boolean {
    return this.has(key);
  }

  /**
   * 获取当前对象的所有值，保持原始键名格式。
   * @returns {T} - 以原始键名存储的数据对象。
   */
  public getValues<T extends KrRecordable = KrRecordable>(): T {
    const data: Partial<T> = {};
    for (const key of Object.keys(this)) {
      // 仅获取实例本身的属性，避免原型链上的属性被加入
      if (Object.prototype.hasOwnProperty.call(this, key)) {
        data[key as keyof T] = this.get(key) as T[keyof T];
      }
    }
    return this.transformData(data) as T;
  }
  /**
   * 转换对象格式：
   * - 如果对象的键都是数字（类似 `{0: {...}, 1: {...}}`），则转换为数组。
   * - 如果对象的键包含非数字（如 `{name: "xxx", age: "18"}`），则保持原结构。
   * @param { T } data - 需要转换的对象
   * @returns { T | Array<any> } 转换后的数组或原始对象
   */
  private transformData<T extends Record<string, any>>(data: T): T | Array<any> {
    // 判断是否是索引对象（所有键是否都是数字）
    const isArrayLike = Object.keys(data).every((key) => /^\d+$/.test(key));

    if (isArrayLike) {
      return Object.values(data); // 转换为数组
    } else {
      return data; // 维持原结构
    }
  }

  /**
   * 规范化键名（去除空格并按需转换为小写）。
   * @param {string} key - 需要处理的键。
   * @returns {string} - 规范化后的键名。
   */
  private getNormalizeKey(key: string): string {
    return this[normalizeKeySymbol] ? key.trim().toLowerCase() : key.trim();
  }
}